<?

/** Printing  related classes and functions.
 *  Note! This uses soPdf and therefore requires pdflib support php.
 *
 *  This collection of functions (libary) is licensed under GPL2.
 *  See LICENSE or www.gnu.org for more details.
 *
 *  @author Jyry Kuukkanen
 *  $Id: dzprint.php,v 1.12 2005/11/10 19:31:04 jyry Exp $
 */

include_once("soset.php");
include_once("soinifiles.php");
include_once("sostring.php");
include_once("soformat.php");
include_once("sopdf.php");
include_once("dzsrcentity.php");
include_once("sogeneric.php");


/** Template sections and keys */
define(SOPT_SYS_SECTION, "sys");
define(SOPT_STYLES_SECTION, "styles");
define(SOPT_GROUPS_SECTION, "groups");

/** Printing areas, also template section names */
define(SOPA_HEAD, "head");
define(SOPA_FOOT, "foot");
define(SOPA_IMAGES, "images");
define(SOPA_BOXES, "boxes");
define(SOPA_DETAILS, "details");
define(SOPA_TOTALS, "totals");

/* Column total functions */
define(SOPF_SUM, "sum");
define(SOPF_AVG, "avg");

/* Misc */
define(DZP_SPACES, " -,./");


/** Print style structure
 */
class dzBasicPrintItem {
    var $Width;      /** dec Print box widht */
    var $Height;     /** dec Print box height */
    var $X;          /** dec Printing X coordinate */
    var $Y;          /** dec Printing Y coordinate or offset y-value in row
                      *  details */
}; // dzBasicPrintItem


/** Basic print style structure
 */
class dzPrintStyle {
    var $FontName;   /** str Font name */
    var $FontSize;   /** str Font size */
    var $Align;      /** SOTA_* Alignment */
    var $Attr;       /** int SOFA_* Font attributes */
    var $Emphasis;   /** str SOFE_* Font emphasis */

    function dzPrintStyle() {
        $this->FontName = "Helvetica";
        $this->FontSize = 10;
        $this->Align = SOTA_LEFT;
    } // dzPrintStyle

}; // dzPrintStyle


/** Print item strucuture for report row printing
 */
class dzPrintItem extends dzBasicPrintItem {
    var $StyleName;     /** str Style name to use */
    var $Style;         /** dzprintStyle Style object to use */
    var $Prefix;        /** str text to print before the actual value */
    var $Postfix;       /** str text to print after the actual value */
    var $Formula;       /** str Optional formula */
    var $TotalFormula;  /** str Optional formula for total columns */
    var $Datatype;      /** str Datatype name (text, int, dec, date, time,
                         *  datetime -- defaults to text (="")
                         */
    var $NumFmt;        /** array/int Number format: array(total_len, decimals)
                         */
}; // dzPrintItem


/** Print item strucuture for head/foot printing
 */
class dzPrintItemX extends dzPrintItem {
    var $Head;             /** dec Column head */
    var $HeadStyle;        /** dzprintStyle Style object to use for col head */
}; // dzPrintItem


/** Image print item
 */
class dzImagePrintItem extends dzBasicPrintItem {
    var $File;
    var $Scale;
}; // dzImagePrintItem


/** Box print item
 */
class dzBoxPrintItem extends dzBasicPrintItem {
    var $LineWidth;
}; // dzImagePrintItem


/** Document printing class
 *  @var object soIni $Template Template ini file.
 *  @var string $Language Language code.
 */
class dzPrint {
    var $__Template;
    var $__PageSize = "";      /** str "a4", etc */
    var $__Encoding = "";      /** str "host", "buildin", "winansi" or
                                *  "macroman" */
    var $__DocOpen = 0;        /** bool Flag to indicate if doc is started but
                                *  not commited/cancelled yet */
    var $__DetailsTop;         /** int Details top row position */
    var $__DetailsBottom;      /** int Details bottom row position */
    var $__DetailsLeft;        /** int Details left most col x */
    var $__DetailsRight;       /** int Details right most col x */
    var $__DetailsBorderWidth; /** int Details border line width */
    var $__DetailsBorder;      /** array/str Details border lines, an array of
                                *  key words "left", "right", "top" or
                                *  "bottom". */
    var $__LeftMargin;         /** int Left margin in points */
    var $__TopMargin;          /** int Top margin in points */
    var $__Pdf;                /** soPdf Pdf object */
    var $__Styles;             /** soSet/dzPrintStyle Printing styles */
    var $__AreaNames;          /** array/str Printing area names */
    var $__Images;             /** array/dzImagePrintItem Images */
    var $__Boxes;              /** array/dzBasicPrintItem Boxes */
    var $__Areas;              /** array/soset/dzPrintItem Print areas and
                                *  their items ("head", "foot", "details") */
    var $__Groups;             /** soSet/dzGroup (Filter) groups */
    var $__RowY;               /** dec Current row Y pos */
    var $__PageNo = 1;         /** int Current page no. */
    var $__RowHeight;          /** dec Details row height */
    var $__MemoRowHeight;      /** dec Memo row height */
    var $__Style;              /** str Default style name */
    var $__Page;               /** array/any Page margins and size as in:
                                *  array("left" => int, "top" => int,
                                *  "size" => str)
                                */
    var $__FontEncoding;       /** str font encoding name. */
    var $__BreakCols;          /** soSet/DZPB_* Break-on columns and functions
                                *  to apply on break. */


    /** Constructor
     *  @param string $Template soStream source
     *  @param string $Language language code
     *  @param string $Unit Unit (mm, cm, m...) to use in dimensions and positions.
     */
    function dzPrint($Template, $Language, $Unit = "mm") {
        include("sodebug.inc");

        $this->__Areas = array();
        $this->__Groups = new soSet;
        $this->__Images = array();
        $this->__Boxes = array();
        $this->__BreakCols = new soSet();
        $this->__Template = new soIni($Template);
        $split = soSplitStr($Template, ".", -1, 1);
        $this->__Translations = new soIni($split[0]."txt");
        $this->__Language = $Language;

        /* [sys] ]*/
        $this->__Style =
                $this->__Template->getValue(SOPT_SYS_SECTION, "style",
                                            SOINI_NOSUB);
        $this->__RowHeight =
            intval($this->__Template->getValue(SOPT_SYS_SECTION,
                                               "rowheight", SOINI_NOSUB));
        $value = $this->__Template->getValue(SOPT_SYS_SECTION,
                                             "memorowheight", SOINI_NOSUB);
        if (!$value) $value = $this->__RowHeight;
        $this->__MemoRowHeight = intval($value);

        $value = $this->__Template->getValue(SOPT_SYS_SECTION,
                                             "details", NULL);
        if (($value != NULL) && ($value->size())) {
            $this->__DetailsTop = intval($value->getItem("top"));
            $this->__DetailsBottom = intval($value->getItem("bottom"));

            /* Border format in ini is
             * "border=linewidth,top+left+right+bottom" */
            $border = soExplode($value->getItem("border"), ",");
            $this->__DetailsBorderWidth = intval($border[0]);
            $this->__DetailsBorder = soExplode($border[1], "+");
        }; // if

        /* Get unit */
        $value = $this->__Template->getValue(SOPT_SYS_SECTION, "unit",
                                             SOINI_NOSUB);
        if ($value) $Unit = $value;

        /* Get locale */
        $this->__Locale =
            $this->__Template->getValue(SOPT_SYS_SECTION, "locale",
                                        SOINI_NOSUB);

        /* Get locale */
        $this->__FontEncoding =
            $this->__Template->getValue(SOPT_SYS_SECTION, "font_encoding",
                                        SOINI_NOSUB);

        /* Page setup */
        $this->__Page = array();
        $value = $this->__Template->getValue(SOPT_SYS_SECTION, "page", NULL);
        $this->__Page["left"] = intval($value->getItem("left"));
        $this->__Page["top"] = intval($value->getItem("top"));
        $this->__Page["size"] = $value->getItem("size");

        /* [groups] */
        $this->__setupGroups();

        /* others */
        $this->__Styles = new soSet();
        $this->__setupStyles();
        $this->__setupAreas();
        $this->__setupImages();
        $this->__setupBoxes();

        $this->__Pdf = new soPdf($Unit);
    } // dzPrint


    /** Sets up filter groups
     */
    function __setupGroups() {
        $i = $this->__Template->getValueCount(SOPT_GROUPS_SECTION);
        while (--$i >= 0) {
            $name = $this->__Template->getValueName(SOPT_GROUPS_SECTION, $i);
            $set = $this->__Template->getValue(SOPT_GROUPS_SECTION, $i, NULL);

            $grp = new dzGroup();
            $grp->Type = DZGT_SUB;

            /* filtering */
            $opt_str = $set->getItem(DZCP_FILTER_SEED);
            if ($opt_str) $grp->Filter->Seed = soExplode($opt_str, ",");
            $opt_str = $set->getItem(DZCP_FILTER_POT);
            if ($opt_str) $grp->Filter->Pot = soExplode($opt_str, ",");

            /* Save group */
            $this->__Groups->addItem($name, $grp);
        }; // while
    } // __setupGroups


    /** Sets a break-o column and it's function
     *  >$Name:str Column name to break on
     *  >$Func:SOPF_* Function to apply
     *  <:bool Returns true(1) if function type is valid, otherwise false(0).
     */
    function setBreakCol($Name, $Func) {
        if (!in_array($Func, array(SOPF_SUM, SOPF_AVG))) {
            return 0; // <-- EXIT
        }; // if

        $this->__BreakCols->setItem($Name, $Func);
        return 1;
    } // setBreakCol


    /** Returns alignment constant for a string.
     *  Note! Returns SOTA_ALIGN for anything dodgy.
     *  >$AlignTxt:str Alignment string used in ini files.
     *  <:SOTA_* Predefined constant for specified string.
     */
    function getAlign($AlignTxt) {
        switch ($AlignTxt) {
            case "right":
                $result = SOTA_RIGHT;
                break;
            case "centre":
            case "center":
                $result = SOTA_CENTRE;
                break;
            case "justify":
                $result = SOTA_JUSTIFY;
                break;
            case "fulljustify":
                $result = SOTA_FULLJUSTIFY;
                break;
            default: // left
                $result = SOTA_LEFT;
        }; // switch

        return $result;
    } // getAlign


    /** Sets up styles from ini data
     */
    function __setupStyles() {
        $i = 0;
        do {
            $set = $this->__Template->getValue(SOPT_STYLES_SECTION, $i, NULL);
            $error = $this->__Template->Error;
            if (!$error) {

                /* Copy style from another, if specified so */
                $value = $set->getItem("copy");
                if ($value != NULL) $style = $this->__Styles->getItem($value);
                if ($style == NULL) $style = new dzPrintStyle();

                /* Set properties specified in style section */
                $value = $set->getItem("font");
                if ($value != NULL) $style->FontName = $value;
                $value = $set->getItem("size");
                if ($value != NULL) $style->FontSize = intval($value);
                $value = $set->getItem("align");
                if ($value != NULL) $style->Align = $this->getAlign($value);
                $value = $set->getItem("width");
                if ($value != NULL) $style->Width = $value;
                $value = $set->getItem("height");
                if ($value != NULL) $style->Height = $value;

                $style->Emphasis = intval($style->Emphasis);
                $value = $set->getItem("bold", "bool");
                if ($value != NULL) $style->Emphasis += SOFE_BOLD;
                $value = $set->getItem("italic", "bool");
                if ($value != NULL) $style->Emphasis += SOFE_ITALIC;

                $style->Attr = 0;
                $value = $set->getItem("underline", "bool");
                if ($value != NULL) $style->Attr += SOFA_UNDERLINE;
                $value = $set->getItem("overline", "bool");
                if ($value != NULL) $style->Attr += SOFA_OVERLINE;
                $value = $set->getItem("strikeout", "bool");
                if ($value != NULL) $style->Attr += SOFA_STRIKEOUT;

                $this->__Styles->addItem(
                    $this->__Template->getValueName(
                        SOPT_STYLES_SECTION, $i), $style);
            }; // if

            $i++;
        } while (!$error);
    } // __setupStyles


    /** Sets up images from ini data
     */
    function __setupImages() {
        $icount = $this->__Template->getValueCount(SOPA_IMAGES);
        for ($i = 0; $i < $icount; $i++) {
            $set = $this->__Template->getValue(SOPA_IMAGES, $i, NULL);
            if ($set != NULL) {
                $item = new dzImagePrintItem();
                $split = soSplitStr($set->getItem("xy"), ",", 1);
                $item->X = doubleval($split[0]);
                $item->Y = doubleval($split[1]);
                $item->Scale = doubleval($set->getItem("scale"));
                if (!$item->Scale) $item->Scale = 1;
                $item->File = str_replace("~", dzGetMainScriptPath(),
                                          $set->getItem("file"));
                $this->__Images[] = $item;
            }; // if
        }; // for
    } // __setupImages


    /** Sets up boxes from ini data
     */
    function __setupBoxes() {
        $icount = $this->__Template->getValueCount(SOPA_BOXES);
        for ($i = 0; $i < $icount; $i++) {
            $value = $this->__Template->getValue(SOPA_BOXES, $i, 0);
            if ($value) {
                $item = new dzBoxPrintItem();
                list($x, $y, $w, $h, $t) = split(",", $value);
                $item->X = doubleval($x);
                $item->Y = doubleval($y);
                $item->Width = doubleval($w);
                $item->Height = doubleval($h);
                $item->LineWidth = doubleval($t);
                $this->__Boxes[] = $item;
            }; // if
        }; // for
    } // __setupBoxes


    /** Sets up areas from ini data
     */
    function __setupAreas() {
        /* Setup print areas */
        $this->__AreaNames = array(SOPA_HEAD, SOPA_FOOT,
                                   SOPA_DETAILS, SOPA_TOTALS);
        foreach ($this->__AreaNames as $area) {
            $this->__Areas[$area] = new soSet();

            $icount = $this->__Template->getValueCount($area);
            for ($i = 0; $i < $icount; $i++) {
                $set = $this->__Template->getValue($area, $i, NULL);
                if ($set != NULL) {
                    if (in_array($area, array(SOPA_DETAILS, SOPA_TOTALS))) {
                        $item = new dzPrintItemX();
                    } else {
                        $item = new dzPrintItem();
                    };
                    $item->StyleName = $set->getItem("style");
                    if (!$item->StyleName) {
                        $item->StyleName = $this->__Style;
                    }; // if

                    $item->Style = $this->__Styles->getItem($item->StyleName);
                    if ($item->Style == NULL) {
                        $item->Style = new dzPrintStyle();
                    };
                    $value = $set->getItem("width");
                    if ($value != NULL) $item->Width = $value;
                    $value = $set->getItem("align");
                    if ($value) $item->Style->Align =
                        $this->getAlign($value);

                    if (in_array($area, array(SOPA_DETAILS, SOPA_TOTALS))) {
                        $item->X = $set->getItem("x");
                        $item->Y = $set->getItem("y");
                        $item->Head = $this->__translate($set->getItem("head"));
                        $head_style = $set->getItem("headstyle");
                        $item->HeadStyle =
                            $this->__Styles->getItem($head_style);
                        if ($item->HeadStyle == NULL) {
                            $item->HeadStyle = new dzPrintStyle();
                        };

                        /* Set details left most point: the smaller the better
                         * but no less than nough */
                        $pos = $item->X;
                        if ($this->getAlign($item->Style->Align) ==
                            SOTA_RIGHT) {
                            $pos = $pos -$item->Width;
                        }; // if
                        $this->__DetailsLeft = max(min(0, $pos), 0);
                    } else {
                        $item->X = soExtractVal($set->getItem("xy"), ",", 1);
                        $item->Y = soExtractVal($set->getItem("xy"), ",", 2);
                    }; // else if
                    $item->NumFmt = $set->getItem("numfmt");

                    $value = $set->getItem("formula");
                    if ($value != NULL) {
                        if ($area == SOPA_TOTALS) {
                            /* Totals require pre-processing before normal
                             * calculations */
                            $item->TotalFormula = $value;
                        };
                        $item->Formula = $value;
                    };

                    if (in_array($area, array(SOPA_DETAILS, SOPA_TOTALS))) {
                        $this->__DetailsRight = max($this->__DetailsRight,
                                                    $item->X +$item->Width);
                    };

                    $value = $set->getItem("height");
                    if ($value != NULL) $item->Height = $value;

                    /* Get prefix & postfix texts */
                    $item->Prefix =
                        $this->__translate(str_replace("\\s", " ",
                                           $set->getItem("prefix")));
                    $item->Postfix =
                        $this->__translate(str_replace("\\s", " ",
                                           $set->getItem("postfix")));

                    /* Override style definitions if necessary */
                    $value = $set->getItem("font");
                    if ($value) $item->Style->FontName = $value;
                    $value = $set->getItem("size");
                    if ($value) $item->Style->FontSize = $value;
                    $style->FontSize = $set->getItem("size");

                    $name = $this->__Template->getValueName($area, $i);
                    $type = soExtractVal($name, ":", 2);
                    $name = soExtractVal($name, ":", 1);
                    if (!$type) $type = "text";
                    $item->Datatype = $type;
                    $this->__Areas[$area]->addItem($name, $item);
                }; // if

            }; // for
        }; // foreach
    } // __setupAreas


    /** Translates a text, if translation available
     *  @param string $Txt Text to translate.
     *  @return string Text translated or original when translation not
     *  available.
     */
    function __translate($Txt) {
        $result = $Txt;
        if (($Txt) && ($this->__Translations)) {
            $trans =
                $this->__Translations->getValue($result, $this->__Language, 0);
            if ($trans) $result = $trans;
        }; // if
        return $result;
    } // __translate


    /** Starts a new document
     *  >$Left:int Left margin
     *  >$Top:int Top margin
     *  >$PageSize:str Page size code: "a4"...
     *  <:bool Returns true(1) if parameters are correct or false(0) if not.
     */
    function beginDoc() {
        if ($this->__DocOpen) return 0; // <-- EXIT

        $this->__RowY = $this->__DetailsTop;
        $this->__DocOpen = 1;
        $this->__Pdf->beginDoc($this->__Page["left"], $this->__Page["top"],
                               $this->__Page["size"], $this->__FontEncoding);
    } // beginDoc


    /** Closes document.
     *  Common code for cancel/commitDoc
      */
    /** Commits and print a document.
     *  >$Output:str Target output stream name
     */
    function commitDoc($Output) {
        $this->__DocOpen = 0;
        $this->__Pdf->commitDoc($Output);
    } // commitDoc


    /** Cancels and resets a document
     */
    function cancelDoc() {
        $this->__DocOpen = 0;
        $this->__Pdf->cancelDoc($Output);
    } // commitDoc


    /** Start a new page in the document.
     *  <:bool Returns true(1) on success, or false(0) on error.
     */
    function nextPage(&$ArraySet, $Simulate = 0) {
        $this->__RowY = $this->__DetailsTop;
        $this->__PageNo++;
        if ($this->__Pdf->nextPage()) {
            $this->__printFixedAreas($ArraySet, $Simulate);
            $this->__printImages($Simulate);
            $this->__printBoxes($Simulate);
            $this->__print_details_heads($Simulate);
            return 1;
        } else {
            return 0;
        };
    } // beginPage


    /** Returns array of col names of an area.
     *  Areas are head, foot, etc. This returns column names from template for
     *  specified area.
     *  >$Area:SOPA_* Area.
     *  <:array/str Column names.
     */
    function __getColNames($Area) {
        return $this->__Areas[$Area]->getKeys();
    } // __getColNames


    /** Prints specified fixed area column
     *  Prints the 1st value of column.
     *  >$Area:SOPA_* Area to print
     *  >$Name:str Column name
     *  >$Value:str Column value lines (\n)
     *  >$ArraySet:soArraySet Columns to operate on.
     *  >$Simulate:bool When set(1), simulates printing instead of doing it.
     */
    function __printFixedCol($Area, $Name, $Value, &$ArraySet, $Simulate = 0) {
        if ($Simulate) return; // <-- EXIT

        $pitem = $this->__Areas[$Area]->getItem($Name);
        $this->__Pdf->setFont($pitem->Style->FontName,
                              $pitem->Style->FontSize,
                              $pitem->Style->Emphasis);
        if ($pitem->Formula) {
            $Value = $this->__getCalcValue($pitem->Formula, 0, $ArraySet);
        }; // if

        /* Print all rows, especially with memo, and print at least one row */
        if ($pitem->Width) $Value = $this->__wrapText($Value, $pitem->Width);

        $icount = max(soValCount($Value, "\n"), 1);
        for ($i = 0; $i < $icount; $i++) {
            $this->__Pdf->placeText($pitem->Prefix.
                $this->__formatValue($Area, $Name,
                soExtractVal($Value, "\n", $i +1)).$pitem->Postfix,
                $pitem->X, $pitem->Y +($i *$this->__MemoRowHeight),
                $pitem->Style->Align,
                $pitem->Width, $pitem->Height);
        }; // for
    } // __printFixedCol


    /** Wraps a text to match the specified width.
     *  The width specified is in printing units and this function splits
     *  the text provided into as many rows as required to fit the width.
     *  @param string $Text Text to wrap.
     *  @param int $Width Width to fit.
     *  @return string Wrapped text.
     */
    function __wrapText($Text, $Width) {
        $txt = soExplode($Text, "\n");
        $result = array();
        foreach ($txt as $row) {
            if ($this->__Pdf->getTextWidth($row) > $Width) {
                while (($pos = $this->__findSplitPos($row, $Width)) != -1) {
                    $result[] = substr($row, 0, $pos);
                    $row = substr($row, $pos +1);
                }; // while
            } else {
                $result[] = $row;
            };
        }; // foreach

        return implode("\n", $result);
    } // __wrapText


    /** Looks for usable break point in a string.
     *  Tries to find a char pos in a string before $Width to fit.
     *  @param string $Text Text to check.
     *  @param int $Width Width to check against.
     *  @return int Char pos, -1==no valid char pos before Width
     */
    function __findSplitPos($Text, $Width) {
        $result = -1;
        $pos = 0;
        $txt = "";
        $text_len = strlen($Text);
        if (!$text_len) return -1; // <-- EXIT

        do {
            $char = substr($Text, $pos, 1);
            $w = $this->__Pdf->getTextWidth($txt.$char);
            if ($w <= $Width) {
                if (soStrPos(DZP_SPACES, $char, 1) != -1) $result = $pos;
                $txt .= $char;
            };
        } while (($w <= $Width) && ($pos++ < $text_len));
        return $result;
    } // __fundSplitPos


    /** Prints head/foot and details.
     *  >$ArraySet:soArraySet All columns to print
     *  >$Simulate:bool When set(1), simulates printing instead of doing it.
     *  <:bool Returns true(1) if input params are valid, otherwise false(0).
     */
    function printOut(&$ArraySet, $Simulate = 0) {
        if (soGetClass($ArraySet) != "soarrayset") return 0; // <-- EXIT
        $purkki = new dzPurkki();
        $purkki->setupGroups($this->__Groups);
        $purkki->setStore(TISKI, $ArraySet);

        /* Handle formula columns:
         * 1) Calc normal calcs - totals might not get right without this
         * 2) Calc total calcs - some of these depend on other formulas
         * 3) Recalc normal calcs - lastly, calc again with totals
         */
        $this->__calcCols($ArraySet, array(SOPA_DETAILS));
        $this->__calcTotals($ArraySet);
        $this->__calcCols($ArraySet, array(SOPA_TOTALS));

        /* Print images and possible border lines for details area */
        $this->__printImages($Simulate);

        /* Print boxes */
        $this->__printBoxes($Simulate);

        /* Print fixed areas */
        $this->__printFixedAreas($ArraySet, $Simulate);

        /* Print details col heads */
        $this->__print_details_heads($Simulate);

        /* Print details (rows) */
        $keys = $this->__getColNames(SOPA_DETAILS);
        $cols = soExtractFromSet($ArraySet, $keys);
        $rows = array();
        $this->rotateSetGrid($cols, $rows, 1);
        $icount = count($rows);
        for ($i = 0; $i < $icount; $i++) {
            if (soGetClass($rows[$i]) == "soset") {
                $this->__print_section(SOPA_DETAILS, $rows[$i],
                                       $ArraySet, $Simulate);
            }; // if
        }; // for $i

        /* Print total rows */
        $keys = $this->__getColNames(SOPA_TOTALS);
        $cols = soExtractFromSet($ArraySet, $keys);
        $rows = array();
        $this->rotateSetGrid($cols, $rows, 1);
        $icount = count($rows);
        for ($i = 0; $i < $icount; $i++) {
            if (soGetClass($rows[0]) == "soset") {
                $this->__print_section(SOPA_TOTALS, $rows[0],
                                       $ArraySet, $Simulate);
            }; // if
        }; // for $i

        return 1;
    } // printOut


    /** Prints out head/foot
     */
    function __printFixedAreas(&$ArraySet, $Simulate = 0) {
        $areas = array(SOPA_HEAD, SOPA_FOOT);
        foreach ($areas as $area) {
            $keys = $this->__getColNames($area);
            foreach ($keys as $key) {
                $value = $ArraySet->getArrayItem($key, 0);
                $this->__printFixedCol($area, $key, $value, $ArraySet,
                                       $Simulate);
            }; // foreach key
        }; // foreach area
    } // __printFixedAreas


    /** Print images
     */
    function __printImages($Simulate = 0) {
        if ($Simulate) return; // <-- EXIT

        if (count($this->__DetailsBorder)) {
            foreach ($this->__DetailsBorder as $border) {
                switch ($border) {
                    case "top":
                        $x = $this->__DetailsLeft
                             -$this->__DetailsBorderWidth;
                        $y = $this->__DetailsTop -$this->__RowHeight
                             -$this->__DetailsBorderWidth;
                        $this->__Pdf->placeLine($this->__DetailsBorderWidth,
                            $x, $y,
                            $this->__DetailsRight, $y);
                            break;
                    case "bottom":
                        $x = $this->__DetailsLeft
                             -$this->__DetailsBorderWidth;
                        $y = $this->__DetailsBottom
                             +$this->__DetailsBorderWidth;
                        $this->__Pdf->placeLine($this->__DetailsBorderWidth,
                            $x, $y,
                            $this->__DetailsRight, $y);
                        break;
                    case "left":
                        $x = $this->__DetailsLeft
                             -$this->__DetailsBorderWidth;
                        $this->__Pdf->placeLine($this->__DetailsBorderWidth,
                            $x,
                            $this->__DetailsTop -$this->__RowHeight
                             -$this->__DetailsBorderWidth,
                            $x,
                            $this->__DetailsBottom
                             +$this->__DetailsBorderWidth);
                            break;
                    case "right":
                        $x = $this->__DetailsRight
                             +$this->__DetailsBorderWidth;
                        $this->__Pdf->placeLine($this->__DetailsBorderWidth,
                            $x,
                            $this->__DetailsTop -$this->__RowHeight
                             -$this->__DetailsBorderWidth,
                            $x,
                            $this->__DetailsBottom
                             +$this->__DetailsBorderWidth);
                            break;
                }; // switch
            }; // foreach
        };
        if (!count($this->__Images)) return; // <-- EXIT

        foreach ($this->__Images as $image) {
            $this->__Pdf->placeImage($image->File, $image->X, $image->Y,
                                     $image->Scale);
        }; // foreach
    } // __printImages


    /** Print boxes
     */
    function __printBoxes($Simulate = 0) {
        if ($Simulate) return; // <-- EXIT

        foreach ($this->__Boxes as $box) {
#            if (!soIsEmptyStr($box->Width)) {
                $this->__Pdf->placeLine($box->LineWidth, $box->X, $box->Y, 
                    $box->X +$box->Width, $box->Y);
#            };
#            if (!soIsEmptyStr($box->Height) && !soIsEmptyStr($box->Width)) {
                $this->__Pdf->placeLine($box->LineWidth,
                    $box->X, $box->Y +$box->Height, 
                    $box->X +$box->Width, $box->Y +$box->Height);
                $this->__Pdf->placeLine($box->LineWidth,
                    $box->X +$box->Width, $box->Y, 
                    $box->X +$box->Width, $box->Y +$box->Height);
#            };
#            if (!soIsEmptyStr($box->Height)) {
                $this->__Pdf->placeLine($box->LineWidth,
                    $box->X, $box->Y, 
                    $box->X, $box->Y +$box->Height);
#            };
        }; // foreach
    } // __printBoxes


    /** Print details column heads
     *  >$Simulate:bool When set(1), simulates printing instead of doing it.
     */
    function __print_details_heads($Simulate = 0) {
        if ($Simulate) return; // <-- EXIT

        $keys = $this->__Areas[SOPA_DETAILS]->getKeys();
        foreach ($keys as $key) {
            $pitem = $this->__Areas[SOPA_DETAILS]->getItem($key);
            if ($pitem->Head) {
                $this->__Pdf->setFont($pitem->HeadStyle->FontName,
                                      $pitem->HeadStyle->FontSize,
                                      $pitem->HeadStyle->Emphasis,
                                      $pitem->HeadStyle->Attr);
                $this->__Pdf->placeText($pitem->Head, $pitem->X,
                                        $this->__DetailsTop
                                        -$this->__RowHeight
                                        -$this->__DetailsBorderWidth *2,
                                        $pitem->Style->Align, $pitem->Width,
                                        $pitem->Height);
            };
        }; // foreach
    } // __print_details_heads


    /** Returns formatted value based datatype/format settings.
     *  >$ColName:str Column name
     *  >$Value:any Value to format according to ColName
     *  <:str Formatted value.
     */
    function __formatValue($Area, $ColName, $Value) {
        $pitem = $this->__Areas[$Area]->getItem($ColName);
        switch ($pitem->Datatype) {
            case "dec":
            case "int":
                if ($pitem->NumFmt) {
                    $result = soNumFmt($Value,
                        intval(soExtractVal($pitem->NumFmt, ",", 1)),
                        intval(soExtractVal($pitem->NumFmt, ",", 2)));
                } else {
                    $result = $Value;
                };
                break;
            case "date":
                $fmt_str = dzGetIniItem("general", "date", "app");
                $result = soFormatTime($Value, "date", $fmt_str);
                break;
            case "time":
                $fmt_str = dzGetIniItem("general", "time", "app");
                $result = soFormatTime($Value, "time", $fmt_str);
                break;
            case "datetime":
                $fmt_str = dzGetIniItem("general", "datetime", "app");
                $result = soFormatTime($Value, "datetime", $fmt_str);
                break;
            default:
                $result = $Value;
        }; // switch

        return $result;
    } // __formatValue


    /** Prints section columns
     *  >$Area:SOPA_DETAILS|SOPA_TOTALS Are to print section from.
     *  >$Section:soSet Section columns to print. Key=colname, value=txt to
     *  print
     *  >$Simulate:bool When set(1), simulates printing instead of doing it.
     */
    function __print_section($Area, $Section, &$ArraySet, $Simulate = 0) {
        /* Format the area columns (num/date/etc) */
        $keys = $Section->getKeys();
        foreach ($keys as $key) {
            $value = $Section->getItem($key);
            $Section->setItem($key, $this->__formatValue($Area, $key, $value));
        }; // foreach

        $rowcount = $this->__getSectionRowCount($Area, $keys) +1;
        for ($i = 0; $i < $rowcount; $i++) {
            /* Sort out the columns for current rowno. */
            $row_cols = array();
            foreach ($keys as $key) {

                $pitem = $this->__Areas[$Area]->getItem($key);
                if (intval($pitem->Y) == $i) $row_cols[] = $key;
            }; // foreach

            $this->__print_row($Area, $Section, $row_cols, $ArraySet, $Simulate);
        }; // for $i
    } // __print_section


    /** Returns max row count in section columns
     *  Each column has Y value (unit is 'rows', defaults to 0), so that some
     *  columns may be printed below the baseline. This function check each
     *  column Y value to find out how many rows should be printed in the
     *  section.
     *  >$Area:SOPA_DETAILS|SOPA_TOTALS
     *  >$ColNames:array/str Column names to check.
     *  <:int Row count to print.
     */
    function __getSectionRowCount($Area, $ColNames) {
        $result = 0;
        foreach ($ColNames as $col_name) {
            $pitem = $this->__Areas[$Area]->getItem($col_name);
            $result = max($result, intval($pitem->Y));
        }; // foreach

        return $result;
    } // __getSectionRowCount


    /** Prints section row columns
     *  >$Row:soSet Row columns to print. Key=colname, value=txt to print
     *  >$ColNames:array/str Column names to print
     *  >$Simulate:bool When set(1), simulates printing instead of doing it.
     */
    function __print_row($Area, $Row, $ColNames, &$ArraySet, $Simulate = 0) {
        $rowcount = max(1, $this->__getMemoRowCount($Row, $ColNames));
        for ($i = 0; $i < $rowcount; $i++) {

            foreach ($ColNames as $key) {
                if (substr($key, 0, 1) != SO_DOLLAR) {
                    $pitem = $this->__Areas[$Area]->getItem($key);
                    if ($pitem != NULL) {
                        $value = soExtractVal($Row->getItem($key), "\n", $i +1);
                        $this->__Pdf->setFont($pitem->Style->FontName,
                                              $pitem->Style->FontSize,
                                              $pitem->Style->Emphasis);
                        $this->__Pdf->placeText($pitem->Prefix.$value.
                                                $pitem->Postfix,
                                                $pitem->X, $this->__RowY,
                                                $pitem->Style->Align,
                                                $pitem->Width,
                                                $pitem->Height);
                    }; // if $pitem
                }; // if $key
            }; // foreach
            if (($this->__RowY +$this->__RowHeight) > $this->__DetailsBottom) {
                $this->nextPage($ArraySet);
            } else {
                $this->__RowY = $this->__RowY +$this->__MemoRowHeight;
            };
        }; // for $i
    } // __print_row


    /** Returns max row count of multi row columns (memo cols).
     *  Any row column may have one or more '\n' delimited rows to print.
     *  This function finds the tallest of them.
     *  >$Row:soSet Row columns to check.
     *  >$ColNames:array Col names that belong to the same row.
     *  <:
     */
    function __getMemoRowCount($Row, $ColNames) {
        $result = 0;
        foreach ($ColNames as $key) {
            $result = max($result, soValCount($Row->getItem($key), "\n"));
         }; // foreach

        return $result;
    } // __getSubRowCount


    /** Returns total values (sum/avg/...)
     *  >$ArraySet:soArraySet Columns to print
     */
    function __calcTotals(&$ArraySet) {
        $keys = $this->__getColNames(SOPA_TOTALS);
        $values = soExtractFromSet($ArraySet, $keys);
        $funcs = array("sum", "avg");
        foreach ($keys as $key) {
            $pitem = $this->__Areas[SOPA_TOTALS]->getItem($key);
            $formula = $pitem->TotalFormula;
            if ($formula) {
                foreach ($funcs as $func) {
                    $src_tag = soParseVal($formula, $func."(", ")", 1, 1);
                    if ($src_tag) {
                        $src_col = soParseVal($src_tag, "\$", "\$", 1);
                        $col_arr = $ArraySet->getItem($src_col);
                        if (!$ArraySet->Error) {
                            switch ($func) {
                                case SOPF_SUM:
                                    $value = soSum($col_arr);
                                    break;
                                case SOPF_AFG:
                                    $value = soAvg($col_arr);
                                    break;
                                default:
                                    $value = "";
                            }; // switch
                            $pitem->Formula =
                                str_replace($func."(".$src_tag.")",
                                           $value, $formula);
                            $this->__Areas[SOPA_TOTALS]->setItem($key, $pitem);
                        }; // if
                    }; // if src_col
                }; // foreach
            }; // if $formula
        }; // foreach

    } // __calcTotal


    /** Rotates a soSet/array combination
     *  Rotates a soSet object each containing an array of values
     *  90 degrees so that an array returned contains a soSet and each item
     *  in it contains the value.
     *  @param object soArraySet $Grid Grid to rotate.
	 *  @param int $RowCountType Row count to get from grid. See soArraySet for
	 *      details.
     *  @return array An array of soSet objects, grid rotated 90 degrees.
     */
    function rotateSetGrid($Grid, &$Rows, $RowCountType) {
        $Rows = array();
        $keys = $Grid->getKeys();
        $kcount = count($keys);
        $icount = $Grid->getRowCount($keys, $RowCountType);
        for ($i = 0; $i < $icount; $i++) {
            $row = new soSet();;
            for ($k = 0; $k < $kcount; $k++) {
                $value = $Grid->getArrayItem($keys[$k], $i);
                $row->addItem($keys[$k], $value);
            }; // for $k
            $Rows[] = $row;
        }; // for $i

    } // rotateSetGrid


    /** Returns item names for an area.
     *  >$Area:SOPA_* Area id
     *  >$WIthDatatype:bool When set, returns column names with datatype
     *  <:array/str Area column names
     */
    function getAreaColNames($Area, $WIthDatatype = 0) {
        if (!in_array($Area, array_keys($this->__Areas))) {
            return array(); // <-- EXIT
        }; // if

        $result = $this->__Areas[$Area]->getKeys();

        /* Should datatypes be add to each item name? */
        if ($WIthDatatype) {
            $icount = count($result);
            for ($i = 0; $i < $icount; $i++) {
                $item = $this->__Areas[$Area]->getItem($result[$i]);
                $type = $item->Datatype;
                $result[$i] = $result[$i].":$type";
            }; // foreach
        }; // if

        return $result;
    } // getAreaColNames


    /** Returns template ini data
     *  Used by dzReport
     *  <:$soiniData Template ini data
     */
    function _getTemplate() {
        return $this->__Template;
    } // _getTemplate


    /** Loops columns and calculates values by formula
     *  Some columns may have formula specified, and this function executes
     *  them.
     *  >$ArraySet:soArraySet Columns to loop.
     */
    function __calcCols(&$ArraySet, $Areas) {
        foreach ($Areas as $area_id) {
            $keys = $this->__Areas[$area_id]->getKeys();
            if (!count($keys)) continue; // <-- CONTINUE

            foreach ($keys as $key) {
                $pitem = $this->__Areas[$area_id]->getItem($key);
                if ($pitem->Formula) {
                    $this->__calcFormula($pitem->Formula, $key, $ArraySet);
                }; // if
            }; // foreach pitem
        }; // foreach area
    } // __calcCols


    /** Returns column names found in a formula.
     *  Find all possible and valid source columns from formula
     *  >$Formula:str Formula
     *  >$ArraySet:soArraySet Columns to operate on.
     *  <:array/str Column names found in formula
     */
    function __getFormulaCols($Formula, &$ArraySet) {
        $i = 1;
        $src_col = soParseVal($Formula, '$', '$', $i);
        $result = array();
        while ($src_col) {
            if ($ArraySet->itemExists($src_col)) $result[] = $src_col;
            $src_col = soParseVal($Formula, '$', '$', ++$i);
        }; // while

        return $result;
    } // __getFormulaCols


    /** Calculates values for specified column.
     *  >$Formula:str Formula to calculate a value by
     *  >$TargetCol:str Target column name
     *  >$ArraySet:soArraySet Columns to operate on.
     */
    function __calcFormula($Formula, &$TargetCol, &$ArraySet) {

        /* 1. Get all valid column names from formula */
        $sources = $this->__getFormulaCols($Formula, $ArraySet);

        /* 2. Loop by tallest srccol and set value for each target col row */
        if (count($sources)) {
            $icount = $ArraySet->getRowCount($sources, 1);
        } else {
            $icount = $ArraySet->getRowCount(array($TargetCol), 1);
            if (!$icount) $icount = 1;
        };

        for ($i = 0; $i < $icount; $i++) {
/*            $f = $Formula;
            foreach($sources as $src_col) {
                $src_val = $ArraySet->getArrayitem($src_col, $i);
                $f = str_replace('$'.$src_col.'$', $src_val, $f);
            }; // foreach
            $target_val = "";
echo "formula==$Formula/$f<br>";
            eval("\$target_val = $f;");
            $ArraySet->setArrayItem($TargetCol, $i, $target_val);
*/
            $ArraySet->setArrayItem($TargetCol, $i,
                                    $this->__getCalcValue($Formula, $i,
                                                          $ArraySet));
        }; // for $i
    } // __calcFormula


    /** Returns calculated value
     *  This is the actual formula computer that gets it's source values from
     *  specified row no in source columns specifgied by the formula.
     *  >$Formula:str Formula to calculate
     *  >$RowNo:int Row no. to get source values from-.
     *  >$ArraySet:soArraySet Columns to operate on.
     *  <:any Formula result
     */
    function __getCalcValue($Formula, $RowNo, &$ArraySet) {
        $split = soSplitStr($Formula, "eval://", 1);
        if (!count($split)) {
            $f = "\$result = $Formula;";
        } else {
            $split = soExplode($split[1], ":");
            $split[0] = str_replace("~", dzGetMainScriptPath(), $split[0]);
            $f = "include_once(\"".$split[0]."\"); \$result = ".
                               $split[1]."(\$ArraySet, $RowNo);";
        }; // else if
        $sources = $this->__getFormulaCols($f, $ArraySet);
        foreach($sources as $src_col) {
            $src_val = $ArraySet->getArrayitem($src_col, $RowNo);
            $f = str_replace('$'.$src_col.'$', $src_val, $f);
        }; // foreach

        $result = "";
        @eval($f);
        return $result;
    } // __getCalcValue


    /** Returns area id for specified column.
     *  >$ColName:str Column to find.
     *  <:str/NULL Returns area id or NULL if not found.
     */
    function __findArea($ColName) {
        $icount = count($this->__AreaNames);
        $result = NULL;
        $i = 0;
        do {
            if ($this->__Areas[$this->__AreaNames[$i]]->itemExists($ColName)) {
                $result = $this->__AreaNames[$i];
            }; // if
        } while (($result == NULL) && (++$i < $icount)); // for

        return $result;
    } // __finddArea

}; // dzPrint

?>
